import { h, Schema } from "koishi"
import { Temporal } from "temporal-polyfill"

import { DynamicTool } from "@langchain/core/tools"
import { ChatLunaPlugin } from "koishi-plugin-chatluna/services/chat"
import {
  fuzzyQuery,
  getMessageContent,
} from "koishi-plugin-chatluna/utils/string"

export const name = "-hengge-classtable"

export const Config = Schema.object({
  timezone: Schema.string().default("PRC"),
  firstWeek: String, // 2025-08-31
  periods: Schema.array(Schema.object({
    id: String,
    start: String, // 08:00
  })),
  courses: Schema.array(Schema.object({
    name: String,
    remark: Schema.string(),
    times: Schema.array(Schema.object({
      days: Schema.union([String, Schema.array(Number)]),
      excludeWeeks: Schema.union([Schema.string(), Schema.array(Number)]),
      remark: Schema.string(),
      start: String, // #periodId or 08:00
    })),
  })),
  weekDayOverrides: Schema.dict(Number), // 2025-09-28: 5
})

export const inject = ["cron"]

function checkAnpb(expr, t) {
  if (!(Number.isInteger(t) && t > 0)) return false
  const m = expr.match(/^\s*((?:(0|-?[1-9]\d*|-)\s*)?n(?:\s*\+(?=\s*\d))?)?\s*(0|[1-9]\d*)?\s*$/)
  if (!m) throw new Error(`Invalid expression ${JSON.stringify(expr)}`)
  const a = m[1] ? m[2] === "-" ? -1 : Number(m[2] || 1) : 0
  const b = Number(m[3] || 0)
  return Math.sign((t - b) * a) !== -1 &&
    (a ? (t - b) % a === 0 : t === b)
}

function formatTimeSpec(spec, value) {
  const time = Temporal.PlainTime.from(value).toLocaleString("zh-CN", { timeStyle: "short" })
  if (spec[0] === "#") return `第 ${spec.slice(1)} 节（${time}）`
  return ` ${time} `
}

const weekDays = "日一二三四五六日"

export function apply(ctx, data) {
  function getStartEndTime(time, prop) {
    const value = time[prop]
    if (value.startsWith("#")) {
      const id = value.slice(1)
      return data.periods.find(p => p.id === id)[prop]
    }
    return value
  }

  function getTimetable(day) {
    const weekDay = Object.hasOwn(data.weekDayOverrides, day)
      ? data.weekDayOverrides[day]
      : day.dayOfWeek
    const week = Math.floor(day.since(data.firstWeek).total("day") / 7) + 1
    const classes = []
    for (const course of data.courses) {
      let { name, remark, isClass = true } = course
      for (const time of course.times) {
        if (!time.days.includes(weekDay) || !isClass) continue
        if (time.remark) remark = time.remark
        if (time.weeks) {
          if (typeof time.weeks === "string") {
            if (!checkAnpb(time.weeks, week)) continue
          } else if (Array.isArray(time.weeks)) {
            if (!time.weeks.includes(week)) continue
          }
        }
        if (time.excludeWeeks?.includes(week)) continue

        const start = Temporal.PlainDateTime.from(
          day.toString() +
          "T" +
          getStartEndTime(time, "start")
        )
        const end = Temporal.PlainDateTime.from(
          day.toString() +
          "T" +
          getStartEndTime(time, "start")
        )
        classes.push({
          name,
          remark,
          start,
          end,
          course,
          time,
        })
      }
    }
    classes.sort((a, b) => Temporal.PlainDateTime.compare(a.start, b.start))
    return { day, weekDay, week, classes }
  }

  function initTasks(day) {
    const { classes } = getTimetable(day)
    for (const { name, remark, start } of classes) {
      const notifyTime = start.toZonedDateTime(data.timezone).subtract({ minutes: 25 }).withTimeZone(Temporal.Now.timeZoneId())
      if (Temporal.ZonedDateTime.compare(notifyTime, Temporal.Now.zonedDateTimeISO()) <= 0) continue
      if (!remark) continue
      const cronExp = `${notifyTime.second} ${notifyTime.minute} ${notifyTime.hour} ${notifyTime.day} ${notifyTime.month} *`
      ctx.logger.info(cronExp, { name, remark, start: start.toString(), notifyTime: notifyTime.toString() })
      const stop = ctx.cron(cronExp, () => {
        stop()
        ctx.bots["onebot:2748080608"].sendMessage("860386808", h.text(`【${remark}】${start.toLocaleString("zh-Hans", { timeStyle: "short" })} ${name}`))
      })
    }
    ctx.logger.info("done init tasks for", day.toString())
  }

  const launchDay = Temporal.Now.instant().toZonedDateTimeISO(data.timezone).toPlainDate()
  initTasks(launchDay)
  ctx.cron("0 6 * * *", () => {
    const today = Temporal.Now.instant().toZonedDateTimeISO(data.timezone).toPlainDate()
    if (today.equals(launchDay)) return
    initTasks(today)
  })

  ctx.logger.debug(formatTimetable(launchDay))
  ctx.logger.debug(formatTimetable(launchDay.add({ days: 1 })))
  ctx.logger.debug(formatTimetable(launchDay.add({ days: 2 })))
  ctx.logger.debug(formatTimetable(launchDay.add({ days: 3 })))

  function formatTimetable(day) {
    //ctx.logger.debug("format timetable", day.toString())
    const { week, weekDay, classes } = getTimetable(day)
    const lines = classes.map(({ name, remark, start, end, time }, i) => {
      const arr = [name]
      if (remark) arr.push(remark)
      arr.push(`${formatTimeSpec(time.start, start)}到${formatTimeSpec(time.end, end)}`.trim())
      const str = `${i + 1}. ${arr.join("，").trim()}`
      //ctx.logger.debug("timetable line", i + 1, { name, remark, start: start.toString(), end: end.toString(), time }, str)
      return str
    })
    const realWeekDay = day.dayOfWeek % 7
    const topLine = [
      `${day}`,
      `第 ${week} 周`,
      `星期${weekDays[realWeekDay]}`,
    ]
    if (weekDay % 7 !== realWeekDay) {
      if (weekDay % 7 >= 1 && weekDay % 7 <= 5)
        topLine.push(`由于调休，按星期${weekDays[weekDay % 7]}课表上课`)
      else topLine.push("放假")
    }
    topLine.push(lines.length ? "课程表：" : "无课。")
    //ctx.logger.debug("timetable topLine", topLine.join("，").trim())
    lines.unshift(topLine.join("，").trim())
    return lines.join("\n")
  }

  ctx.inject(["chatluna"], ctx => {
    const plugin = new ChatLunaPlugin(ctx, {}, "plugin-hengge-classtable", false)
    ctx.on("ready", async () => {
      plugin.registerTool("hengge_classtable", {
        selector(history) {
          const ok = history.slice(-10).some(item => {
            const content = getMessageContent(item.content)
            return fuzzyQuery(
              content,
              ["课", "上什么", "教室", "哪上", "几点", "今天", "明天", "后天", "周", "星期"]
            )
          })
          ctx.logger.debug("select", ok)
          return ok
        },
        authorization(session) {
          const ok = ctx.filter(session)
          ctx.logger.debug("authorization", ok)
          return ok
        },
        createTool(params, session) {
          ctx.logger.debug("createTool")
          return new DynamicTool({
            name: "hengge_classtable",
            description: "你正在跟哼歌（LNN的外号）和他的同班同学对话，你可以用此工具查询他们班级某一天的课程表。" +
              "输入必须是一个格式如“2024-01-02”的日期，且距离今天必须在 7 天以内。" +
              "选修课会带有“(选)”标记，这些课程只有部分同学需要去上。" +
              "工具也会给出指定的这一天所在的教学周序号和放假、调休情况。",
            func(input) {
              try {
                ctx.logger.debug("call tool", { input })
                const today = Temporal.Now.instant().toZonedDateTimeISO(data.timezone).toPlainDate()
                const day = input ? Temporal.PlainDate.from(input) : today
                if (Math.abs(day.since(today).total("day")) > 7)
                  return `${day} 距离今天（${today}）超过 7 天，无法查询。`
                const result = formatTimetable(day)
                ctx.logger.debug("tool result", result)
                return result
              } catch (err) {
                ctx.logger.error("tool error", { input }, err)
                return "查询过程中发生未知错误。"
              }
            }
          })
        },
      })
    })
  })
}
